package cn.highset.redis.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;


/**
 * redis String 操作工具类
 *
 * @author gg
 * @version RedisStringService.java, v 0.1 2019-04-04 16:21 gg
 */
public class RedisStringService extends RedisBaseService {

    private RedisTemplate<String, Object> redisTemplate;

    public RedisStringService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 对应redis set 命令
     *
     * @param key   key
     * @param value value
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     */
    public void set(final String key, Object value) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value);
    }

    /**
     * 对应redis setEx 命令
     *
     * @param key      key
     * @param value    value
     * @param expire   超时时间
     * @param timeUnit 时间单位
     *                 NANOSECONDS 纳秒
     *                 MICROSECONDS  微秒
     *                 MILLISECONDS 毫秒
     *                 SECONDS 秒
     *                 MINUTES 分钟
     *                 HOURS 小时
     *                 DAYS  天
     * @see <a href="http://redis.io/commands/setex">Redis Documentation: SETEX</a>
     */
    public void setEx(final String key, Object value, long expire, TimeUnit timeUnit) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value, expire, timeUnit);
    }

    /**
     * 对应 redis setRange键偏移值 命令
     * 从偏移量offset开始，用value参数覆写（overwrite）键key储存的字符串值
     * SETRANGE命令会确保字符串足够长以便将value设置到指定的key偏移量上，如果键原来储存的字符串长度比偏移量小
     * （比如字符串只有5个字符长，但你设置的offset是10），那么原字符和偏移量之间的空白将用零字节（zeroBytes，"\x00"）进行填充。
     * 因为Redis字符串的大小被限制在512兆（兆字节）以内，所以用户能够使用的最大偏移量为2 ^ 29-1（536870911），
     * 如果你需要使用比这更大的空间，请使用多个key
     * <p>
     * 当生成一个很长的字符串时，Redis需要分配内存空间，该操作有时候可能会造成服务器阻塞（块）。
     *
     * @param key    key
     * @param value  value
     * @param offset 偏移量
     * @see <a href="http://redis.io/commands/setrange">Redis Documentation: SETRANGE</a>
     */
    public void setRange(final String key, Object value, long offset) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(key, value, offset);
    }

    /**
     * 对应 setNx 命令 如果key 存在，则return false，如果key不存在，则存入value return true
     *
     * @param key   key
     * @param value value
     * @return 命令在设置成功时返回 1 ， 设置失败时返回 0
     * @see <a href="http://redis.io/commands/setnx">Redis Documentation: SETNX</a>
     */
    public Boolean setNx(final String key, Object value) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.setIfAbsent(key, value);
    }

    /**
     * 对应 mSet 命令
     * <p>
     * 同时为多个键设置值。
     * 如果某个给定键已经存在， 那么 MSET 将使用新值去覆盖旧值， 如果这不是你所希望的效果， 请考虑使用 MSETNX 命令，
     * 这个命令只会在所有给定键都不存在的情况下进行设置。
     * MSET 是一个原子性(atomic)操作， 所有给定键都会在同一时间内被设置， 不会出现某些键被设置了但是另一些键没有被设置的情况。
     *
     * @param map Map
     * @see <a href="http://redis.io/commands/mset">Redis Documentation: MSET</a>
     */
    public void mSet(Map<String, Object> map) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.multiSet(map);
    }

    /**
     * 对应 mSetNx 命令
     * <p>
     * 当且仅当所有给定键都不存在时， 为所有给定键设置值。
     * 即使只有一个给定键已经存在， mSetNx 命令也会拒绝执行对所有键的设置操作。
     * mSetNx 是一个原子性(atomic)操作， 所有给定键要么就全部都被设置， 要么就全部都不设置， 不可能出现第三种状态。
     *
     * @param map Map
     * @return 当所有给定键都设置成功时， 命令返回 1 ； 如果因为某个给定键已经存在而导致设置未能成功执行， 那么命令返回 0 。
     * @see <a href="http://redis.io/commands/mset">Redis Documentation: MSET</a>
     */
    public Boolean mSetNx(Map<String, Object> map) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.multiSetIfAbsent(map);
    }

    /**
     * 对应 redis get 命令
     *
     * @param key key
     * @return 如果键 key 不存在， 那么返回特殊值 null ； 否则， 返回键 key 的值。
     * 如果键 key 的值并非字符串类型， 那么返回一个错误， 因为 GET 命令只能用于字符串值。
     * @see <a href="http://redis.io/commands/get">Redis Documentation: GET</a>
     */
    public Object get(final String key) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key);
    }

    /**
     * 对应 getSet 命令
     * 将键key的值设为value，并返回键key在被设置之前的旧值。
     *
     * @param key   key
     * @param value Object
     * @return 返回给定键 key 的旧值。
     * 如果键 key 没有旧值， 也即是说， 键 key 在被设置之前并不存在， 那么命令返回 null 。
     * 当键 key 存在但不是字符串类型时， 命令返回一个错误。
     * @see <a href="http://redis.io/commands/getset">Redis Documentation: GETSET</a>
     */
    public Object getSet(final String key, Object value) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.getAndSet(key, value);
    }

    /**
     * 对应 mGet 命令
     * 返回给定的一个或多个字符串键的值。
     * 如果给定的字符串键里面， 有某个键不存在， 那么这个键的值将以特殊值 null 表示。
     *
     * @param keys Collection
     * @return 命令将返回一个列表， 列表中包含了所有给定键的值。
     * @see <a href="http://redis.io/commands/mget">Redis Documentation: MGET</a>
     */
    public List<Object> mGet(Collection<String> keys) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.multiGet(keys);
    }

    /**
     * 对应redis incrBy 命令
     * <p>
     * 如果键key不存在，那么键key的值会先被初始化为0，然后再执行 incrBy 命令。
     * 如果键key储存的值不能被解释为数字，那么 incrBy 命令将返回一个错误。
     * 本操作的值限制在64位（bit）有符号数字表示之内。
     *
     * @param key       key
     * @param increment 增加的偏移量
     * @return 在加上增量 increment 之后， 键 key 当前的值。
     * @see <a href="http://redis.io/commands/incr">Redis Documentation: INCR</a>
     */
    public Long incrBy(final String key, long increment) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.increment(key, increment);
    }

    /**
     * 对应 incrByFloat 命令
     * 如果键 key 不存在， 那么 incrByFloat 会先将键 key 的值设为 0 ， 然后再执行加法操作。
     * 如果命令执行成功， 那么键 key 的值会被更新为执行加法计算之后的新值， 并且新值会以字符串的形式返回给调用者。
     * 此外， 无论加法计算所得的浮点数的实际精度有多长， incrByFloat 命令的计算结果最多只保留小数点的后十七位。
     * <p>
     * 当以下任意一个条件发生时， 命令返回一个错误：
     * 键 key 的值不是字符串类型(因为 Redis 中的数字和浮点数都以字符串的形式保存，所以它们都属于字符串类型）；
     * 键 key 当前的值或者给定的增量 increment 不能被解释(parse)为双精度浮点数。
     *
     * @param key       key
     * @param increment 增加的偏移量
     * @return 在加上增量 increment 之后， 键 key 的值
     * @see <a href="http://redis.io/commands/incrbyfloat">Redis Documentation: INCRBYFLOAT</a>
     */
    public Double incrByFloat(final String key, double increment) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.increment(key, increment);
    }

    /**
     * 如果键key已经存在并且它的值的英文一个字符串， APPEND命令将把value追加到键key现有值的末尾。
     * 如果key不存在， APPEND就简单地将键key的值设为value，就像执行一样。SET key value
     *
     * @param key   key
     * @param value String
     * @return Integer 追加value之后，键key的值的长度。
     * @see <a href="http://redis.io/commands/append">Redis Documentation: APPEND</a>
     */
    public Integer append(final String key, String value) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.append(key, value);
    }

    /**
     * 对应 getRange 命令
     * 返回键key储存的字符串值的指定部分，字符串的截取范围由start和end两个偏移量决定（包括start和end在内）。
     * 负数偏移量表示从字符串的末尾开始计数，-1表示最后一个字符 ， -2表示倒数第二个字符，以此类推。
     *
     * @param key   key
     * @param start 开始
     * @param end   结束
     * @return String  命令会返回字符串值的指定部分。
     * @see <a href="http://redis.io/commands/getrange">Redis Documentation: GETRANGE</a>
     */
    public String getRange(final String key, long start, long end) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.get(key, start, end);
    }

    /**
     * 对应 strLen 命令
     *
     * @param key key
     * @return strLen 命令返回字符串值的长度。
     * 当键 key 不存在时， 命令返回 0 。
     * 当 key 储存的不是字符串值时， 返回一个错误。
     * @see <a href="http://redis.io/commands/strlen">Redis Documentation: STRLEN</a>
     */
    public Long strLen(final String key) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.size(key);
    }

    /**
     * 对应 setBit 命令 属于 位图操作
     * <p>
     * 对 key 所储存的字符串值，设置或清除指定偏移量上的位(bit)。
     * 位的设置或清除取决于 value 参数，可以是 0 也可以是 1 。
     * 当 key 不存在时，自动生成一个新的字符串值。
     * 字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时，空白位置以 0 填充。
     * offset 参数必须大于或等于 0 ，小于 2^32 (bit 映射被限制在 512 MB 之内)。
     * <p>
     * 对使用大的 offset 的 setBit 操作来说，内存分配可能造成 Redis 服务器被阻塞。
     *
     * @param key    key
     * @param offset 偏移量
     * @param value  设置或清除
     * @return 指定偏移量原来储存的位
     * @see <a href="http://redis.io/commands/setbit">Redis Documentation: SETBIT</a>
     */
    public Boolean setBit(final String key, long offset, boolean value) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.setBit(key, offset, value);
    }

    /**
     * 对应 getBit 命令
     * <p>
     * 对 key 所储存的字符串值，获取指定偏移量上的位(bit)。
     * 当 offset 比字符串值的长度大，或者 key 不存在时，返回 0 。
     *
     * @param key    key
     * @param offset 偏移量
     * @return 字符串值指定偏移量上的位(bit)
     * @see <a href="http://redis.io/commands/setbit">Redis Documentation: GETBIT</a>
     */
    public Boolean getBit(final String key, long offset) {
        checkKey(key);
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        return valueOperations.getBit(key, offset);
    }

}